/*
	jGrouse framework
	Copyright 2007 by Denis Riabtchik 
	All rights reserved.
	
	Licensed under modified BSD license. See license.txt for details
*/


/**
	@module jgrouse.widgets.TemplateParser
	@requires jgrouse.dom.Element
	@requires jgrouse.util.Maps
	@requires jgrouse.util.Catalog
*/

jgrouse.module(
{
	name : 'jgrouse.widgets.TemplateParser',
	requires : ['jgrouse.dom.Element', 'jgrouse.util.Maps', 'jgrouse.util.Catalog', 'jgrouse.widgets.Layout'],
	imports	 : ['jgrouse.dom.Element', 'jgrouse.util.SimpleMap', 
		'jgrouse.util.ObjectMap', 'jgrouse.util.Catalog', 'jgrouse.string.*'],
	body : function(embed)
	{
		eval(embed);
		
		/**
			Class that parses HTML template and creates jGrouse widgets from that template.
			For each element TemplateParser would analyse attributes according to the following rules:
			<ul>
				<li>If element does not have attribute ID, then the system would generate that ID</li>
				<li>If element does have attribute ID, then the system would replace it with generated 
					id and attach the original ID as suffix. For example, if original ID was called
					lastName, then the generated ID would look like jg439043-01.lastName</li>
				<li>If element does have attribute that has value starting with idref: then it is assumed
				that the element references another by ID and the value would be replaced with generated 
				ID that corresponds to the referenced one. For example, if the original template had 
				a control with ID lastName and a label that was referencing that control via attribute 
				FOR="idref:lastName" then the attribute would be replaced with something like 
				FOR="jg343435-10.lastName"</li>
				<li>If element has attribute _jgtype then the value of the attribute should be the alias
				of widget's class. For example, _jgtype="inputText" would create a widget with class 
				jgrouse.widgets.InputText</li>
				<li>If element has attribute _jgmember then the rendered widget becomes member of owner container with name
				matching the value of _jgmember attribute</li>
				<li>if the attribute has name starting with _ (underscore) but not with _jg, then TemplateParser would interpret that 
				as a setter of widget's property. For example _tag-name="span" would result to call 
				widget's method setTagName("span") (note that attribute names are always converted to lowercase first and then camelized). 
				If corresponding method was not found, the framework would throw an exception.</li>
				<li>If attribute name starts with $ then it is treated either as spice declaration or setting 
				an attribute of spice. For example <code> $hover="rollover" </code> would attach a spice with 
				name of <code>$hover</code> and type (alias) of <code>rollover</code>. The expression 
				<code> $hover.class-name='hovering'</code> is equivalent to finding a spice with name $hover and 
				invoking it's method setClassName('hovering')</li>
				<li>If attribute name is "style" then TemplateParser would parse the style attributes and
				pass them to widget's constructor in "styles" parameter.
				<li> All attributes with names that do not start with _ are passed to widget's constructor
				as "options" parameter</li>
			</ul>
			If widget declares method setTemplateManager then it would be called passing this as a parameter.
			This is done in cases when internals of the new widget should be integrated into template's owner MVC.<br/>
			
			If widget declares method setTemplate then it would be called and innerHTML of the widget's node would be 
			passed as parameter. This is done in cases when widget would maintain its own MVC.
			
			
			@class jgrouse.widgets.TemplateParser
			@extends jgrouse.widgets.Layout
		*/
		jgrouse.define('jgrouse.widgets.TemplateParser', 'jgrouse.widgets.Layout', function(salt)
		{
			eval(salt);
			
			return {
				/**
					@constructor initialize
					@paramset Initialize with text string
					@param {String} text - text of the template
					@paramset Initialize with DOM nodes
					@param {DOMElement} data - DOMElement containing nodes that should be used as template
				*/
				initialize : function(data)
				{
					_super(this, 'initialize');
					
					/**
						Root of template
						@variable {DOMElement} _root
					*/
					this._root = null;
					
					/**
						Key is the original ID, value is generated ID.
						@variable {jgrouse.util.Map} _idMap
					*/
					this._idMap = new SimpleMap();
					/**
						Catalog of widgets referencing certain ID.
						@variable {jgrouse.util.Catalog} _idRef
					*/
					this._idRef = new Catalog();
					/**
						Map of generated IDs to corresponding DOM nodes.
						Key is generated ID, value is DOM node. 
						@variable {jgrouse.util.Map} _idDomMap
					*/
					this._idDomMap = new SimpleMap();
					/**
						Node that is currently being parsed
						@variable {DOMElement} _currentNode
					*/
					this._currentNode = null;
					
					/**
						Map of widgets that should become members of container.
						Key is member name, value is widget
						@variable {jgrouse.util.Map} _members
					*/
					this._members = new SimpleMap();

					/**
					 * DOM Element to which the layout is attached
					 * @variable {DOMElement} ?
					 */
					this._parentDom = null;	

					/**
					 * Array of {index, widget} sorted by index
					 * @variable {Array} _orderedWidgets
					 */
					this._orderedWidgets = [];					
					
					this._initRoot(data);
				},
				
				_initRoot : function(data)
				{
					var root = document.createElement('div');
					root.className = '____templateRoot';
					if (typeof data == 'string')
					{
						root.innerHTML = data;
					}
					else
					{
						Element.moveChildrenToNewParent(root, data);
					}
					this._root = root;					
					this._currentNode = root;
				},
				
				/**
					Return all widgets that should become members of parent container
					@function {jgrouse.util.Map} getMembers
					@returns map where key is member name and value is widget
				*/
				getMembers : function()
				{
					return this._members;
				},
				
				
				
				/**
					Pars template 
					@function doParse
				*/
				doParse : function()
				{
					/*var athis = this;
					Element.forEach(this._root, function(node, index)
					{
						athis._processNode(node);
					});*/
					
					var athis = this;
					function runThis(node)
					{
						athis._processNode(node);
					}
					
					this._traverse(this._root, runThis)
					this._resolveIdrefs();
				},
				
				_processOneNode : function(domNode)
				{
					if (domNode.nodeType != Element.ELEMENT_NODE)
					{
						return;
					}
					var attrs = Element.getAttributes(domNode);
					var standard = {};
					var custom = {};
					var spice = {};
					for (var i = attrs.length - 1; i >= 0; i--)
					{
						var attr = attrs[i];
						var n = attr.nodeName;
						if (standard[n] !== undefined || custom[n] !== undefined)
						{
							continue
						}
						var v = attr.value;
						var nl = n.toLowerCase();
						if (nl != n)
						{
							Element.clearAttribute(domNode, n);
							Element.setAttribute(domNode, nl, v);
							n = nl;
						}
						if (nl != 'style')
						{
							if (nl == 'id')
							{
								var newVal = jgrouse.widgets.nextId() + '.' + v;
								this._idMap.put(v, newVal);
								attr.value = newVal;
								v = newVal;
								this._idDomMap.put(newVal, domNode);
							}
							else
							{
								var v1 = v.toLowerCase();
								if (v1.indexOf('idref:') === 0)
								{
									var val = v1.substr(6);
									var ref = val.split('.')[0];
									this._idRef.add(ref, {ownerElement: domNode, name:attr.name, value:val});
								}
							}
							if (n.charAt(0) == '_')
							{
								custom[n] = v;
							} else
							if (n.charAt(0) == '$')
							{
								spice[n] = v;
							}
							else
							{	
								standard[n] = v;
							}
						}
					}
					var member = domNode;
					this.customize(domNode, standard, custom, spice);
					if (custom._jgtype)
					{
						var widget = this._createWidget(domNode, standard, custom, spice);
						this.addWidget(widget, domNode);
						member = widget;
					}
					if (custom._jgmember)
					{
						this._members.put(custom._jgmember, member);
					}					
				},
				
				_traverse : function(root, callback)
				{
					var node = root.firstChild;
					var stack = [];
					while (node)
					{
						callback(node);
						var ns = node.firstChild;
						if (ns)
						{
							stack.push(node);
						}
						else
						{
							ns = node.nextSibling;
							if (!ns && stack.length > 0)
							{
								do
								{
									ns = stack.pop();
									ns = ns.nextSibling;
								} while (!ns && stack.length > 0);
							} 
						}
						node = ns;
					}
				},
				
				_processNode : function(domNode)
				{
					var oldCurrent = this._currentNode;
					this._currentNode = domNode;
					this._processOneNode(domNode);
					/*var athis = this;
					Element.forEach(domNode, function(node)
					{
						athis._processNode(node);
					})*/
					
					this._currentNode = oldCurrent;
				},
				
				
				
			/**
				 * Customize attributes of node 
				 * @function customize
				 */
				customize : function(domNode, standard, custom, spice)
				{
					// for overrides in subclasses
				},
				
				_createWidget : function(node, standard, custom, spice)
				{
					//var node = this._currentNode;
					var widgetType = custom._jgtype;
					var widgetClass = jgrouse.widgets.getWidgetClass(widgetType);
					if (!widgetClass)
					{
						throw new jgrouse.Exception("Cannot find class for widget " + widgetType);
					}
					var styles = Element.getStyles(node);
					var widget = new widgetClass(standard, styles);
					if (widget.setTagName)
					{
						widget.setTagName(node.tagName);
					}
					
					function processSpice(spiceAlias, spiceName)
					{
						var sn = spiceName.split('.');
						if (sn.length == 1)
						{
							var spiceInstance = widget.getSpice(spiceName);
							if (!spiceInstance)
							{
								widget.addSpice(spiceName, spiceAlias);
							}
						}
						else
						{
							var spiceInstance = widget.getSpice(sn[0]);
							if (!spiceInstance)
							{
								var refSpiceAlias = spice[sn[0]];
								if (!refSpiceAlias)
								{
									throw 'Cannot find spice definition for ' + spiceName;
								}
								processSpice(refSpiceAlias, sn[0]);
								spiceInstance = widget.getSpice(sn[0]);
							}
							var setter = 'set' + upperFirst(camelize(sn[1]));
							if (!spiceInstance[setter])
							{
								throw 'Cannot find setter ' + setter + ' for spice ' + sn[0];
							}
							spiceInstance[setter](spiceAlias);
						}
					}
					
					jgrouse.forEach(spice, processSpice);
					
					jgrouse.forEach(custom, function(value, name)
					{
						if (name.indexOf('_jg') != 0)
						{
							var n = 'set' + upperFirst(camelize(name.substring(1)));
							if (widget[n])
							{
								widget[n](value);
							}
							else
							{
								throw new jgrouse.Exception('Method ' + n + ' not found in ' + widget.RTTI);
							}
						}
					});
					if (widget.setTemplate)
					{
						widget.setTemplate(this);
					}
					else 
					{
						if (node.firstChild)
						{
							node.innerHTML = '';
						}
					}
					return widget;
				},
				
				_resolveIdrefs : function()
				{
					var athis = this;
					this._idRef.forEach(function(attr, oldId)
					{
						var newVal = athis._idMap.get(oldId);
						var val = attr.value.split('.');
						val[0] = newVal;
						var v = val.join('.');
						attr.value = v;
						var attrDom = attr.ownerElement;
						var widget = athis._widgetsMap.get(attrDom);
						if (widget)
						{
							widget.setAttribute(attr.name, v);
						}
						else
						{
							Element.setAttribute(attrDom, attr.name, v);
						}
					});
					
				},
				
				_domIndex : function(domElement)
				{
					var target = this._parentDom? this._parentDom : this._root;
					var index = 0;
					var node = domElement;
					while (node != target)
					{
						index++;
						node = node.parentNode;
					}
					return index;
				},
				
				_sortWidgets : function(first, second)
				{
					return first.index > second.index? 1 : first.index == second.index? 0 : -1;
				},
				
				/**
					Attach all rendered widgets to given DOM node
					@function render
					@param {DOMElement} parentDom
				*/
				render : function(parentDom)
				{
					this._parentDom = parentDom;
					Element.moveChildrenToNewParent(parentDom, this._root);
					this.forEach(function(widget, dom)
					{
						var domParent = dom.parentNode;
						widget.add(domParent, dom);
					});
				},
				
				/**
					@function addWidget
					@inheritdesc
				*/
				addWidget : function(widget, dom)
				{
					_sup(this, arguments);
					if (this._parentDom)
					{
						widget.add(dom.parentNode, dom);
					}
					var index = this._domIndex(dom);
					this._orderedWidgets.push({index : index, widget : widget});
					this._orderedWidgets.sort(this._sortWidgets);
				},
				
				/**
				 * @function removeWidget
				 * @inheritdesc
				 */
				removeWidget : function(widget, doDispose)
				{
					for (var i = this._orderedWidgets.length; i >= 0; i--)
					{
						if (this._orderedWidgets[i].widget == widget)
						{
							this._orderedWidgets.splice(i, 1);
							break;
						}
					}
					_sup(this, arguments); 
				},
				
				
				/**
					Detach all elements from given DOM node
					@function remove
					@param {DOMElement} parentDom
				*/
				remove : function(parentDom)
				{
					if (this._root)
					{
						Element.moveChildrenToNewParent(this._root, parentDom);
					}
					this._parentDom = null;
					this._orderedWidgets = [];
					_sup(this, arguments);
				},
				
				/**
					Dispose template and reease associated resources
					@function dispose
				*/
				dispose : function()
				{
					_sup(this, arguments);
					this._idMap.clear();
					this._idRef.clear();
					this._idDomMap.clear();
					this._members.clear();
					Element.disposeDom(this._root);
					this._root = null;
				},
				
				/**
					Creates new sub template from current node
					@function subTemplate
				*/
				subTemplate : function()
				{
					return new _class(this._currentNode);
				},
				
				layout : function()
				{
					jgrouse.forEach(this._orderedWidgets, function(item, index){
						var widget = item.widget;
						if (widget.layout)
						{
							widget.layout();
						}
					});
				}
			}
		});
	}
});